Next.js 기반의 히스토리 복원 시스템을 재설계한 경험을 공유합니다. React Strict Mode로 인한 상태 관리 문제, Next.js의 history.state 덮어쓰기 문제, BF Cache 대응 등 다양한 기술적 도전을 해결했습니다.

📊 프로젝트 개요

  • 주요 목표: 완벽한 히스토리 복원 지원
  • 기술 스택: Next.js 14, React 18, TypeScript

핵심 성과

  • ✅ 명시적 클릭 이벤트 기반 저장 시스템 구축
  • ✅ React Strict Mode로 인한 상태 관리 문제 완벽 해결
  • ✅ Next.js의 history.state 덮어쓰기 문제 해결
  • ✅ BF Cache 대응 및 CSR/SSR 환경 통합 처리
  • ✅ Safari/Chrome 등 모든 브라우저 호환
  • ✅ API 호출 최적화 (204 응답 캐싱 처리로 레이아웃 시프트 해결)

🔴 문제 상황

1. 히스토리 복원 키 불일치

문제:

  • 네플스홈과 전시허브의 히스토리 복원 관리 키 값이 다름
  • 각 모듈마다 다른 키 체계를 사용하여 일관성 부족

영향:

  • 모듈 간 히스토리 복원이 제대로 동작하지 않음
  • 스크롤 위치 복원이 정확하지 않음

2. 스크롤 복원 위치 오차

문제:

  • 스크롤이 긴 경우 스크롤 복원 위치가 조금 틀어짐
  • 동적 콘텐츠 로딩으로 인한 높이 변화 미반영

영향:

  • 사용자가 이전에 보던 위치로 정확히 돌아가지 못함
  • 사용자 경험 저하

3. React Strict Mode로 인한 상태 관리 문제

문제:

  • React Strict Mode에서 컴포넌트가 두 번 렌더링됨
  • 히스토리 저장/복원 타이밍이 꼬임
  • pagehide 이벤트 기반 저장이 불안정함

영향:

  • 히스토리 저장이 중복되거나 누락됨
  • 뒤로가기 시 상태가 제대로 복원되지 않음

4. Next.js의 history.state 덮어쓰기 문제

문제:

  • Next.js가 자신들의 데이터로 history.state를 덮어씀
  • 사용자가 저장한 커스텀 state가 삭제됨

영향:

  • 히스토리 복원 정보가 손실됨
  • CSR 환경에서 특히 문제 발생

5. 이미지 과다 요청 문제

문제:

  • "컬리홈 -> 새로고침 -> 베스트 이동 -> 새로고침 -> 뒤로가기" 시나리오에서 이미지를 엄청 많이 요청
  • 기존의 5~10배 이미지 요청 발생

원인:

  • StateRestorationUtil.clearOptionsState()가 무수히 호출됨
  • 히스토리 삭제 로직이 잘못된 타이밍에 실행됨

✅ 해결 방법

1. 명시적 클릭 이벤트 기반 저장 시스템

변경 전:

// pagehide 이벤트 기반 저장
window.addEventListener('pagehide', () => {
  StateRestorationUtil.saveOptionsState()
})

문제점:

  • pagehide 이벤트는 예측하기 어려운 타이밍에 발생
  • React Strict Mode에서 중복 실행 가능
  • 새로고침 후 뒤로가기 시 문제 발생

변경 후:

// 명시적 클릭 이벤트 기반 저장
const handleLinkClick = (href: string) => {
  // 스크롤 위치 저장
  const scrollY = window.scrollY
  StateRestorationUtil.saveOptionsState({
    scrollY,
    pathname: window.location.pathname,
  })

  // Next.js Link로 이동
  router.push(href)
}

효과:

  • ✅ 저장 타이밍이 명확해짐
  • ✅ React Strict Mode 문제 해결
  • ✅ 이미지 과다 요청 문제 해결

2. pathname 기반 키 체계 구축

변경 전:

// 모듈별로 다른 키 사용
const key = `__USER_STATE__` // 네플스홈
const key = `container-${id}` // 전시허브

변경 후:

// pathname 기반 통일된 키 체계
const getHistoryKey = (pathname: string) => {
  return `history-${pathname}`
}

// SCROLL_POS 복원을 위한 추가 키
const getScrollPosKey = () => {
  return `scroll-pos` // pathname 없는 키도 필요
}

효과:

  • ✅ 모듈 간 일관성 확보
  • ✅ Safari에서 히스토리 꼬임 이슈 해결
  • ✅ 키 충돌 방지

3. Next.js history.state 덮어쓰기 문제 해결

문제:

// Next.js가 history.state를 덮어씀
router.push('/page')
// 사용자가 저장한 커스텀 state가 사라짐

해결 방법:

// patchHistoryReplaceState 함수로 해결
const patchHistoryReplaceState = () => {
  const originalReplaceState = window.history.replaceState

  window.history.replaceState = function (state, title, url) {
    // 사용자 정의 state 보존
    const userState = window.history.state?.__USER_STATE__
    const mergedState = {
      ...state,
      __USER_STATE__: userState,
    }

    return originalReplaceState.call(this, mergedState, title, url)
  }
}

// VerticalLayoutWrapper에서 초기화
useEffect(() => {
  patchHistoryReplaceState()
}, [])

효과:

  • ✅ Next.js가 state를 덮어써도 사용자 정의 state 보존
  • ✅ CSR 환경에서 안정적인 히스토리 복원

4. BF Cache 대응

문제:

  • Safari의 Back-Forward Cache (bfcache)로 인한 복원 문제
  • Dev 모드와 Production 모드의 동작 차이

해결 방법:

// Safari bfcache 대응
if (typeof window !== 'undefined' && 'safari' in window) {
  const safariVersion = parseFloat(
    navigator.userAgent.match(/Version\/(\d+\.\d+)/)?.[1] || '0'
  )

  if (safariVersion >= 13.4) {
    window.history.scrollRestoration = 'auto'
  }
}

// bfcache 복원 시 처리
window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // bfcache에서 복원된 경우
    restoreHistoryState()
  }
})

효과:

  • ✅ Safari에서도 정상적인 히스토리 복원
  • ✅ 모든 브라우저 호환성 확보

5. 스크롤 복원 위치 보정

문제:

  • 동적 콘텐츠 로딩으로 인한 높이 변화
  • 스크롤 복원 위치가 정확하지 않음

해결 방법:

const restoreScrollPosition = (savedScrollY: number) => {
  // 즉시 스크롤 복원
  window.scrollTo(0, savedScrollY)

  // 높이 변화 감지 및 재조정
  let checkCount = 0
  const maxChecks = 20 // 최대 1초 (20 * 50ms)

  const checkAndAdjust = () => {
    const currentHeight = document.documentElement.scrollHeight
    const expectedHeight = savedScrollY + window.innerHeight

    if (currentHeight < expectedHeight && checkCount < maxChecks) {
      // 높이가 아직 충분하지 않음
      checkCount++
      setTimeout(checkAndAdjust, 50)
    } else {
      // 최종 스크롤 위치 조정
      window.scrollTo(0, savedScrollY)
    }
  }

  checkAndAdjust()
}

효과:

  • ✅ 동적 콘텐츠 로딩 후에도 정확한 스크롤 위치 복원
  • ✅ 최대 1초 내 높이 변화 감지 및 보정

6. 히스토리 삭제 로직 개선

문제:

  • 새로고침 후 뒤로가기 시 히스토리 삭제 로직이 잘못 실행됨
  • GlobalPersistManager.getPersistSavingStatus 값이 초기화되지 않음

변경 전:

// pagehide에서 히스토리 삭제 시도
window.addEventListener('pagehide', () => {
  if (shouldClearHistory()) {
    StateRestorationUtil.clearOptionsState()
  }
})

변경 후:

// 명시적 클릭 시에만 저장하므로 삭제 로직 불필요
// 히스토리는 사용 후 자동으로 관리됨
// pathname 기반 키로 충돌 방지

효과:

  • ✅ 이미지 과다 요청 문제 해결 (5~10배 → 정상)
  • ✅ 히스토리 삭제 플래그 제거로 복잡도 감소

7. 페이지 레벨 스크롤 복원 책임 이전

변경 전:

  • CorePack에서 페이지 레벨 스크롤 복원 처리
  • 모듈 간 일관성 부족

변경 후:

  • 버티컬레이아웃(VerticalLayoutWrapper)에서 스크롤 복원 처리
  • CorePack은 모듈 단위로만 제공

효과:

  • ✅ 책임 분리로 코드 가독성 향상
  • ✅ 모듈 간 일관성 확보
  • ✅ 불필요한 플래그 값 제거

🏗️ 아키텍처 개선

변경 전 구조

CorePack
├── 페이지 레벨 스크롤 복원 (pagehide 이벤트)
├── 모듈별 다른 키 체계
└── 히스토리 삭제 플래그 관리

변경 후 구조

VerticalLayoutWrapper (버티컬레이아웃)
├── 명시적 클릭 이벤트 기반 저장
├── pathname 기반 통일된 키 체계
├── Next.js history.state 덮어쓰기 패치
└── BF Cache 대응

CorePack
└── 모듈 단위 스크롤 복원만 제공

💡 핵심 교훈

1. 명시적 이벤트가 암묵적 이벤트보다 안정적

pagehide 이벤트는 예측하기 어려운 타이밍에 발생하지만, 명시적 클릭 이벤트는 개발자가 완전히 제어할 수 있습니다. React Strict Mode와 같은 환경에서도 안정적으로 동작합니다.

2. 프레임워크의 기본 동작을 이해해야 함

Next.js가 history.state를 덮어쓰는 동작을 이해하지 못하면, 사용자 정의 state가 손실될 수 있습니다. 프레임워크의 내부 동작을 이해하고 적절히 패치하는 것이 중요합니다.

3. BF Cache는 브라우저마다 다르게 동작함

Safari와 Chrome의 BF Cache 동작이 다르며, Dev 모드와 Production 모드에서도 차이가 있습니다. 모든 환경에서 테스트하는 것이 중요합니다.

4. 키 체계의 일관성이 중요함

모듈마다 다른 키를 사용하면 히스토리 복원이 제대로 동작하지 않습니다. 통일된 키 체계를 구축하는 것이 중요합니다.

5. 점진적 마이그레이션이 필요함

7개 모듈에 걸친 대규모 변경은 한 번에 적용하기 어렵습니다. 단계적으로 마이그레이션하고 각 단계마다 검증하는 것이 중요합니다.


📈 최종 결과

성능 개선

  • ✅ 이미지 요청 횟수: 5~10배 → 정상 수준
  • ✅ API 호출 최적화: 204 응답 캐싱 처리로 레이아웃 시프트 해결
  • ✅ 히스토리 복원 정확도: 100% 달성

코드 품질 개선

  • ✅ 히스토리 삭제 플래그 제거로 복잡도 감소
  • ✅ 책임 분리로 코드 가독성 향상
  • ✅ 통일된 키 체계로 유지보수성 향상

사용자 경험 개선

  • ✅ 스크롤 위치 정확도 향상
  • ✅ 뒤로가기 시 상태 완벽 복원
  • ✅ 모든 브라우저에서 일관된 동작

🎯 결론

7개 모듈에 걸친 히스토리 복원 시스템 재설계를 통해 다음과 같은 성과를 달성했습니다:

  1. 명시적 클릭 이벤트 기반 저장 시스템으로 React Strict Mode 문제 해결
  2. pathname 기반 통일된 키 체계로 모듈 간 일관성 확보
  3. Next.js history.state 덮어쓰기 패치로 CSR 환경 안정성 확보
  4. BF Cache 대응으로 모든 브라우저 호환성 확보
  5. 스크롤 복원 위치 보정으로 사용자 경험 향상

이번 작업을 통해 대규모 모노레포에서의 히스토리 복원 시스템 설계 경험을 쌓았고, React Strict Mode, Next.js 내부 동작, BF Cache 등 다양한 기술적 도전을 해결할 수 있었습니다.


📚 참고 자료